Domine a arte de construir aplicações React resilientes. Este guia explora padrões avançados para compor Suspense e Error Boundaries, permitindo um tratamento de erros granular e aninhado para uma experiência de usuário superior.
Composição de Suspense e Error Boundary no React: Um Mergulho Profundo no Tratamento de Erros Aninhados
No mundo do desenvolvimento web moderno, criar uma experiência de usuário fluida e resiliente é primordial. Os usuários esperam que as aplicações sejam rápidas, responsivas e estáveis, mesmo quando as condições de rede são ruins ou ocorrem erros inesperados. O React, com sua arquitetura baseada em componentes, fornece ferramentas poderosas para gerenciar esses desafios: Suspense para lidar com estados de carregamento e Error Boundaries para conter erros de tempo de execução. Embora poderosos por si só, seu verdadeiro potencial é desbloqueado quando são compostos juntos.
Este guia abrangente levará você a um mergulho profundo na arte de compor Suspense e Error Boundaries no React. Iremos além do básico para explorar padrões avançados de tratamento de erros aninhados, permitindo que você construa aplicações que não apenas sobrevivem a erros, mas que degradam graciosamente, preservando a funcionalidade e proporcionando uma experiência de usuário superior. Esteja você construindo um widget simples ou um painel complexo e cheio de dados, dominar esses conceitos mudará fundamentalmente a forma como você aborda a estabilidade da aplicação e o design da UI.
Parte 1: Revisitando os Blocos Fundamentais
Antes de podermos compor esses recursos, é essencial ter um entendimento sólido do que cada um faz individualmente. Vamos atualizar nosso conhecimento sobre React Suspense e Error Boundaries.
O que é o React Suspense?
Em sua essência, o React.Suspense é um mecanismo que permite que você "espere" declarativamente por algo antes de renderizar sua árvore de componentes. Seu caso de uso primário e mais comum é gerenciar os estados de carregamento associados à divisão de código (usando React.lazy) e à busca de dados assíncrona.
Quando um componente dentro de um limite Suspense suspende (ou seja, sinaliza que não está pronto para renderizar ainda, geralmente porque está esperando por dados ou código), o React sobe na árvore para encontrar o ancestral Suspense mais próximo. Ele então renderiza a prop fallback desse limite até que o componente suspenso esteja pronto.
Um exemplo simples com divisão de código:
Imagine que você tem um componente grande, HeavyChartComponent, que não deseja incluir no seu pacote JavaScript inicial. Você pode usar React.lazy para carregá-lo sob demanda.
// HeavyChartComponent.js
const HeavyChartComponent = () => {
// ... lógica complexa do gráfico
return <div>Meu Gráfico Detalhado</div>;
};
export default HeavyChartComponent;
// App.js
import React, { Suspense } from 'react';
const HeavyChartComponent = React.lazy(() => import('./HeavyChartComponent'));
function App() {
return (
<div>
<h1>Meu Painel</h1>
<Suspense fallback={<p>Carregando gráfico...</p>}>
<HeavyChartComponent />
</Suspense>
</div>
);
}
Neste cenário, o usuário verá "Carregando gráfico..." enquanto o JavaScript para HeavyChartComponent está sendo buscado e analisado. Assim que estiver pronto, o React substitui o fallback pelo componente real de forma transparente.
O que são Error Boundaries?
Um Error Boundary é um tipo especial de componente React que captura erros de JavaScript em qualquer lugar de sua árvore de componentes filhos, registra esses erros e exibe uma UI de fallback em vez da árvore de componentes que travou. Isso impede que um único erro em uma pequena parte da UI derrube toda a aplicação.
Uma característica chave dos Error Boundaries é que eles devem ser componentes de classe e definir pelo menos um de dois métodos de ciclo de vida específicos:
static getDerivedStateFromError(error): Este método é usado para renderizar uma UI de fallback após um erro ter sido lançado. Ele deve retornar um valor para atualizar o estado do componente.componentDidCatch(error, errorInfo): Este método é usado para efeitos colaterais, como registrar o erro em um serviço externo.
Um exemplo clássico de Error Boundary:
import React from 'react';
class MyErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Atualiza o estado para que a próxima renderização mostre a UI de fallback.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Você também pode registrar o erro em um serviço de relatórios de erro
console.error("Erro não capturado:", error, errorInfo);
// logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// Você pode renderizar qualquer UI de fallback personalizada
return <h1>Algo deu errado.</h1>;
}
return this.props.children;
}
}
// Uso:
// <MyErrorBoundary>
// <SomeComponentThatMightThrow />
// </MyErrorBoundary>
Limitação Importante: Error Boundaries não capturam erros dentro de manipuladores de eventos, código assíncrono (como setTimeout ou promessas não ligadas à fase de renderização), ou erros que ocorrem no próprio componente Error Boundary.
Parte 2: A Sinergia da Composição - Por que a Ordem Importa
Agora que entendemos as peças individuais, vamos combiná-las. Ao usar o Suspense para busca de dados, duas coisas podem acontecer: os dados podem carregar com sucesso, ou a busca de dados pode falhar. Precisamos lidar tanto com o estado de carregamento quanto com o estado de erro potencial.
É aqui que a composição de Suspense e ErrorBoundary brilha. O padrão universalmente recomendado é envolver o Suspense dentro de um ErrorBoundary.
O Padrão Correto: ErrorBoundary > Suspense > Componente
<MyErrorBoundary>
<Suspense fallback={<p>Carregando...</p>}>
<DataFetchingComponent />
</Suspense>
</MyErrorBoundary>
Por que essa ordem funciona tão bem?
Vamos rastrear o ciclo de vida de DataFetchingComponent:
- Renderização Inicial (Suspensão):
DataFetchingComponenttenta renderizar, mas descobre que não tem os dados de que precisa. Ele "suspende" lançando uma promessa especial. O React captura essa promessa. - Suspense Assume o Controle: O React sobe na árvore de componentes, encontra o limite
<Suspense>mais próximo e renderiza sua UI defallback(a mensagem "Carregando..."). O error boundary não é acionado porque suspender não é um erro de JavaScript. - Busca de Dados Bem-Sucedida: A promessa é resolvida. O React renderiza novamente
DataFetchingComponent, desta vez com os dados de que precisa. O componente renderiza com sucesso, e o React substitui o fallback do suspense pela UI real do componente. - Busca de Dados Falhou: A promessa é rejeitada, lançando um erro. O React captura esse erro durante a fase de renderização.
- Error Boundary Assume o Controle: O React sobe na árvore de componentes, encontra o
<MyErrorBoundary>mais próximo e chama seu métodogetDerivedStateFromError. O error boundary atualiza seu estado e renderiza sua UI de fallback (a mensagem "Algo deu errado.").
Essa composição lida elegantemente com ambos os estados: o estado de carregamento é gerenciado pelo Suspense, e o estado de erro é gerenciado pelo ErrorBoundary.
O que acontece se você inverter a ordem? (Suspense > ErrorBoundary)
Vamos considerar o padrão incorreto:
<!-- Anti-Padrão: Não faça isso! -->
<Suspense fallback={<p>Carregando...</p>}>
<MyErrorBoundary>
<DataFetchingComponent />
</MyErrorBoundary>
</Suspense>
Essa composição é problemática. Quando DataFetchingComponent suspende, o limite Suspense externo desmontará toda a sua árvore de filhos — incluindo MyErrorBoundary — para mostrar o fallback. Se um erro ocorrer mais tarde, o MyErrorBoundary que deveria capturá-lo pode já ter sido desmontado, ou seu estado interno (como `hasError`) seria perdido. Isso pode levar a um comportamento imprevisível e anula o propósito de ter um limite estável para capturar erros.
Regra de Ouro: Sempre posicione seu Error Boundary fora do Suspense boundary que gerencia o estado de carregamento para o mesmo grupo de componentes.
Parte 3: Composição Avançada - Tratamento de Erros Aninhados para Controle Granular
O verdadeiro poder deste padrão emerge quando você para de pensar em um único error boundary para toda a aplicação e começa a pensar em uma estratégia granular e aninhada. Um único erro em um widget de barra lateral não crítico não deve derrubar toda a página da sua aplicação. O tratamento de erros aninhado permite que diferentes partes da sua UI falhem de forma independente.
Cenário: Uma UI de Painel Complexa
Imagine um painel para uma plataforma de e-commerce. Ele tem várias seções distintas e independentes:
- Um Cabeçalho com notificações do usuário.
- Uma Área de Conteúdo Principal mostrando dados de vendas recentes.
- Uma Barra Lateral exibindo informações do perfil do usuário e estatísticas rápidas.
Cada uma dessas seções busca seus próprios dados. Um erro na busca de notificações não deve impedir o usuário de ver seus dados de vendas.
A Abordagem Ingênua: Um Único Limite no Topo
Um iniciante poderia envolver todo o painel em um único componente ErrorBoundary e Suspense.
function DashboardPage() {
return (
<MyErrorBoundary>
<Suspense fallback={<DashboardSkeleton />}>
<div className="dashboard-layout">
<HeaderNotifications />
<MainContentSales />
<SidebarProfile />
</div>
</Suspense>
</MyErrorBoundary>
);
}
O Problema: Esta é uma experiência de usuário ruim. Se a API para SidebarProfile falhar, todo o layout do painel desaparece e é substituído pelo fallback do error boundary. O usuário perde acesso ao cabeçalho e ao conteúdo principal, mesmo que seus dados possam ter carregado com sucesso.
A Abordagem Profissional: Limites Aninhados e Granulares
Uma abordagem muito melhor é dar a cada seção independente da UI seu próprio wrapper ErrorBoundary/Suspense dedicado. Isso isola falhas e preserva a funcionalidade do resto da aplicação.
Vamos refatorar nosso painel com este padrão.
Primeiro, vamos definir alguns componentes reutilizáveis e um auxiliar para buscar dados que se integra com o Suspense.
// --- api.js (Um wrapper simples de busca de dados para o Suspense) ---
function wrapPromise(promise) {
let status = 'pending';
let result;
let suspender = promise.then(
(r) => {
status = 'success';
result = r;
},
(e) => {
status = 'error';
result = e;
}
);
return {
read() {
if (status === 'pending') {
throw suspender;
} else if (status === 'error') {
throw result;
} else if (status === 'success') {
return result;
}
},
};
}
export function fetchNotifications() {
console.log('Buscando notificações...');
return new Promise((resolve) => setTimeout(() => resolve(['Nova mensagem', 'Atualização do sistema']), 2000));
}
export function fetchSalesData() {
console.log('Buscando dados de vendas...');
return new Promise((resolve, reject) => setTimeout(() => reject(new Error('Falha ao carregar dados de vendas')), 3000));
}
export function fetchUserProfile() {
console.log('Buscando perfil do usuário...');
return new Promise((resolve) => setTimeout(() => resolve({ name: 'Jane Doe', level: 'Admin' }), 1500));
}
// --- Componentes genéricos para fallbacks ---
const LoadingSpinner = () => <p>Carregando...</p>;
const ErrorMessage = ({ message }) => <p style={{color: 'red'}}>Erro: {message}</p>;
Agora, nossos componentes de busca de dados:
// --- Componentes do Painel ---
import { fetchNotifications, fetchSalesData, fetchUserProfile, wrapPromise } from './api';
const notificationsResource = wrapPromise(fetchNotifications());
const salesResource = wrapPromise(fetchSalesData());
const profileResource = wrapPromise(fetchUserProfile());
const HeaderNotifications = () => {
const notifications = notificationsResource.read();
return <header>Notificações ({notifications.length})</header>;
};
const MainContentSales = () => {
const salesData = salesResource.read(); // Isso vai lançar o erro
return <main>{/* Renderizar gráficos de vendas */}</main>;
};
const SidebarProfile = () => {
const profile = profileResource.read();
return <aside>Bem-vindo(a), {profile.name}</aside>;
};
Finalmente, a composição resiliente do Painel:
import React, { Suspense } from 'react';
import MyErrorBoundary from './MyErrorBoundary'; // Nosso componente de classe de antes
function DashboardPage() {
return (
<div className="dashboard-layout">
<MyErrorBoundary fallback={<header>Não foi possível carregar as notificações.</header>}>
<Suspense fallback={<header>Carregando notificações...</header>}>
<HeaderNotifications />
</Suspense>
</MyErrorBoundary>
<MyErrorBoundary fallback={<main><p>Os dados de vendas estão indisponíveis no momento.</p></main>}>
<Suspense fallback={<main><p>Carregando gráficos de vendas...</p></main>}>
<MainContentSales />
</Suspense>
</MyErrorBoundary>
<MyErrorBoundary fallback={<aside>Não foi possível carregar o perfil.</aside>}>
<Suspense fallback={<aside>Carregando perfil...</aside>}>
<SidebarProfile />
</Suspense>
</MyErrorBoundary>
<div>
);
}
O Resultado do Controle Granular
Com esta estrutura aninhada, nosso painel torna-se incrivelmente resiliente:
- Inicialmente, o usuário vê mensagens de carregamento específicas para cada seção: "Carregando notificações...", "Carregando gráficos de vendas..." e "Carregando perfil...".
- O perfil e as notificações carregarão com sucesso e aparecerão em seu próprio ritmo.
- A busca de dados do componente
MainContentSalesfalhará. Crucialmente, apenas seu error boundary específico será acionado. - A UI final mostrará o cabeçalho e a barra lateral totalmente renderizados, mas a área de conteúdo principal exibirá a mensagem: "Os dados de vendas estão indisponíveis no momento."
Esta é uma experiência de usuário vastamente superior. A aplicação permanece funcional, e o usuário entende exatamente qual parte tem um problema, sem ser completamente bloqueado.
Parte 4: Modernizando com Hooks e Projetando Melhores Fallbacks
Embora Error Boundaries baseados em classe sejam a solução nativa do React, a comunidade desenvolveu alternativas mais ergonômicas e amigáveis aos hooks. A biblioteca react-error-boundary é uma escolha popular e poderosa.
Apresentando a `react-error-boundary`
Esta biblioteca fornece um componente <ErrorBoundary> que simplifica o processo и oferece props poderosas como fallbackRender, FallbackComponent, e um callback `onReset` para implementar um mecanismo de "tentar novamente".
Vamos aprimorar nosso exemplo anterior adicionando um botão de tentar novamente ao componente de dados de vendas que falhou.
// Primeiro, instale a biblioteca:
// npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary';
// Um componente de fallback de erro reutilizável com um botão de tentar novamente
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Algo deu errado:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Tentar novamente</button>
</div>
);
}
// Em nosso componente DashboardPage, podemos usá-lo assim:
function DashboardPage() {
return (
<div className="dashboard-layout">
{/* ... outros componentes ... */}
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// redefina o estado do seu query client aqui
// por exemplo, com React Query: queryClient.resetQueries('sales-data')
console.log('Tentando buscar os dados de vendas novamente...');
}}
>
<Suspense fallback={<main><p>Carregando gráficos de vendas...</p></main>}>
<MainContentSales />
</Suspense>
</ErrorBoundary>
{/* ... outros componentes ... */}
<div>
);
}
Ao usar react-error-boundary, ganhamos várias vantagens:
- Sintaxe Mais Limpa: Não é necessário escrever e manter um componente de classe apenas para tratamento de erros.
- Fallbacks Poderosos: As props
fallbackRendereFallbackComponentrecebem o objeto `error` e uma função `resetErrorBoundary`, tornando trivial exibir informações detalhadas sobre o erro e fornecer ações de recuperação. - Funcionalidade de Redefinição: A prop `onReset` integra-se lindamente com bibliotecas modernas de busca de dados como React Query ou SWR, permitindo que você limpe o cache delas e acione uma nova busca quando o usuário clica em "Tentar novamente".
Projetando Fallbacks Significativos
A qualidade da sua experiência de usuário depende muito da qualidade dos seus fallbacks.
Fallbacks de Suspense: Skeleton Loaders
Uma simples mensagem "Carregando..." muitas vezes não é suficiente. Para uma melhor UX, seu fallback de suspense deve imitar a forma e o layout do componente que está carregando. Isso é conhecido como "skeleton loader". Ele reduz a mudança de layout (layout shift) e dá ao usuário uma melhor noção do que esperar, fazendo com que o tempo de carregamento pareça mais curto.
const SalesChartSkeleton = () => (
<div className="skeleton-wrapper">
<div className="skeleton-title"></div>
<div className="skeleton-chart-area"></div>
</div>
);
// Uso:
<Suspense fallback={<SalesChartSkeleton />}>
<MainContentSales />
</Suspense>
Fallbacks de Erro: Acionáveis e Empáticos
Um fallback de erro deve ser mais do que apenas um brusco "Algo deu errado". Um bom fallback de erro deve:
- Ser Empático: Reconheça a frustração do usuário em um tom amigável.
- Ser Informativo: Explique brevemente o que aconteceu em termos não técnicos, se possível.
- Ser Acionável: Forneça uma maneira para o usuário se recuperar, como um botão "Tentar Novamente" para erros de rede transitórios ou um link "Contatar Suporte" para falhas críticas.
- Manter o Contexto: Sempre que possível, o erro deve ser contido dentro dos limites do componente, não tomar a tela inteira. Nosso padrão aninhado alcança isso perfeitamente.
Parte 5: Melhores Práticas e Armadilhas Comuns
Ao implementar esses padrões, tenha em mente as seguintes melhores práticas e armadilhas potenciais.
Lista de Verificação de Melhores Práticas
- Posicione os Limites em Junções Lógicas da UI: Não envolva cada componente individual. Posicione seus pares
ErrorBoundary/Suspenseem torno de unidades lógicas e autocontidas da UI, como rotas, seções de layout (cabeçalho, barra lateral) ou widgets complexos. - Registre Seus Erros: O fallback voltado para o usuário é apenas metade da solução. Use `componentDidCatch` ou um callback em `react-error-boundary` para enviar informações detalhadas do erro para um serviço de logging (como Sentry, LogRocket ou Datadog). Isso é crucial para depurar problemas em produção.
- Implemente uma Estratégia de Redefinição/Tentativa: A maioria dos erros em aplicações web são transitórios (ex: falhas temporárias de rede). Sempre dê aos seus usuários uma maneira de tentar novamente a operação que falhou.
- Mantenha os Limites Simples: Um error boundary em si deve ser o mais simples possível e improvável de lançar um erro próprio. Seu único trabalho é renderizar um fallback ou os filhos.
- Combine com Recursos Concorrentes: Para uma experiência ainda mais suave, use recursos como `startTransition` para evitar que fallbacks de carregamento bruscos apareçam para buscas de dados muito rápidas, permitindo que a UI permaneça interativa enquanto o novo conteúdo é preparado em segundo plano.
Armadilhas Comuns a Evitar
- O Anti-Padrão da Ordem Invertida: Como discutido, nunca coloque
Suspensefora de umErrorBoundaryque se destina a lidar com seus erros. Isso levará à perda de estado e a um comportamento imprevisível. - Confiar nos Limites para Tudo: Lembre-se, Error Boundaries só capturam erros durante a renderização, em métodos de ciclo de vida e nos construtores de toda a árvore abaixo deles. Eles não capturam erros em manipuladores de eventos. Você ainda deve usar blocos
try...catchtradicionais para erros em código imperativo. - Aninhamento Excessivo: Embora o controle granular seja bom, envolver cada pequeno componente em seu próprio limite é um exagero e pode tornar sua árvore de componentes difícil de ler e depurar. Encontre o equilíbrio certo com base na separação lógica de responsabilidades em sua UI.
- Fallbacks Genéricos: Evite usar a mesma mensagem de erro genérica em todos os lugares. Personalize seus fallbacks de erro e carregamento para o contexto específico do componente. Um estado de carregamento para uma galeria de imagens deve ser diferente de um estado de carregamento para uma tabela de dados.
function MyComponent() {
const handleClick = async () => {
try {
await sendDataToApi();
} catch (error) {
// Este erro NÃO será capturado por um Error Boundary
showErrorToast('Falha ao salvar os dados');
}
};
return <button onClick={handleClick}>Salvar</button>;
}
Conclusão: Construindo para a Resiliência
Dominar a composição de React Suspense e Error Boundaries é um passo significativo para se tornar um desenvolvedor React mais maduro e eficaz. Representa uma mudança de mentalidade de simplesmente prevenir que a aplicação trave para arquitetar uma experiência verdadeiramente resiliente e centrada no usuário.
Ao ir além de um único manipulador de erros de nível superior e adotar uma abordagem aninhada e granular, você pode construir aplicações que degradam graciosamente. Recursos individuais podem falhar sem interromper toda a jornada do usuário, os estados de carregamento tornam-se menos intrusivos e os usuários são capacitados com opções acionáveis quando as coisas dão errado. Este nível de resiliência и design de UX atencioso é o que separa as boas aplicações das ótimas no cenário digital competitivo de hoje. Comece a compor, comece a aninhar e comece a construir aplicações React mais robustas hoje.